﻿//The MIT License(MIT)

//Copyright(c) 2015 Martin Chrzan

//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:

//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.

//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace XmlAutoComplete
{
    public class XmlSuggestionEngine : ISuggestionEngine
    {
        #region const strings

        const string START_ELEMENT = "<";
        const string END_ELEMENT = ">";
        const string SIMPLE_END_ELEMENT = "/>";
        const string COMPLEX_END_ELEMENT = "</";

        #endregion


        public  DecisionTree CurrentElement { get; set; }

        DecisionTree Tree { get; set; }
        public XmlSuggestionEngine(DecisionTree tree)
        {
            Tree = tree;
        }


        public List<AutocompleteItem> GetElements(string text, int currentPosition)
        {
            DecisionTree tree = new DecisionTree();

            //element name + opened
            List<Tuple<string, bool>> path = new List<Tuple<string, bool>>();

            var beforeText = text.Substring(0, currentPosition);
            var afterText = "";
            if (currentPosition < text.Length)
            {
                afterText = text.Substring(currentPosition + 1);
            }

            List<ElementInfo> beforeCurrentPosition = ParseText(beforeText);
            List<ElementInfo> afterCurrentPosition = ParseText(afterText);

            var opt = GetOptions(beforeCurrentPosition, afterCurrentPosition);
            return opt;
        }

        private List<AutocompleteItem> GetOptions(List<ElementInfo> beforeCursor, List<ElementInfo> afterCursor)
        {
            var options = new List<AutocompleteItem>();

            var currentElement = Tree;

            var started = new List<ElementInfo>();

            foreach (var element in beforeCursor)
            {
                //dive in
                if (element.State == Status.Start)
                {
                    currentElement = currentElement.Childs.First(it => it.Name == element.Name || (it.Abstract && it.BaseName == element.Name));
                    started.Add(element);
                }

                //move up
                else if (element.State == Status.End)
                {
                    currentElement = currentElement.Parent;
                    if (started.Count > 0 && started.Last() == element)
                        started.RemoveAt(started.Count - 1);
                }

                //set current element
                else if (element.State == Status.Opened)
                {

                    CurrentElement = currentElement.Childs.First(it => it.Name == element.Name || (it.Abstract && it.BaseName == element.Name));

                }
            }

            //might be null on the end of file - when all elements are closed
            if (currentElement != null)
            {
                //added options
                if (currentElement.Childs != null)
                {
                    var startedElement = started.FirstOrDefault();
                    if (startedElement == null)
                        startedElement = new ElementInfo() { Index = 0, Name = "" };

                    //add only when max occur for an element is not reached
                    var counted = CountElements(startedElement, beforeCursor, afterCursor);

                    foreach (var child in currentElement.Childs)
                    {
                        var used = counted.FirstOrDefault(it => it.Key == child.Name);

                        if (!(!used.Equals(default(KeyValuePair<string, int>)) && child.MaxCount <= used.Value) && !string.IsNullOrEmpty(child.Name))
                            options.Add(child);
                    }
                }

                bool addClosingElementOption = true;

                foreach (var element in afterCursor)
                {
                    if ((element.Name == currentElement.Name  || (currentElement.Abstract && element.Name == currentElement.BaseName)) && element.State == Status.End)
                    {
                        //dont add ending element into options
                        addClosingElementOption = false;
                        break;
                    }
                    if (element.Name == currentElement.Name && element.State == Status.Start)
                    {
                        //we found opening element but there was no closing for current 
                        addClosingElementOption = true;
                        break;
                    }
                }

                if (addClosingElementOption && currentElement != null && !string.IsNullOrEmpty(currentElement.Name))
                {
                    var name = currentElement.Name;
                    if (currentElement.Abstract)
                        name = currentElement.BaseName;

                    options.Add(new AutocompleteItem("/" + name + END_ELEMENT));
                }
            }

            return options;

        }

        public Dictionary<string,int> CountElements(ElementInfo started, List<ElementInfo> beforeCursor, List<ElementInfo> afterCursor)
        {
            var countedElements = new Dictionary<string, int>();

            var allElements = beforeCursor.Where(it => it.Index > started.Index).ToList();
            allElements.AddRange(afterCursor);

            var opened = new List<string>();

            foreach (var item in allElements)
            {
                //we dont need to look for others
                if (item.Name == started.Name && item.State == Status.End)
                    return countedElements;

                else if (item.State == Status.End)
                {
                    //the last started element should be closed first, if not something is wrong
                    if (opened.Count > 0 && item.Name == opened.Last())
                        opened.RemoveAt(opened.Count - 1);
                }
                else if(item.State == Status.SimpleClosed || item.State == Status.Start)
                {
                    if (opened.Count == 0)
                    {
                        if (!countedElements.ContainsKey(item.Name))
                            countedElements.Add(item.Name, 0);
                        countedElements[item.Name]++;
                    }

                    if (item.State == Status.Start)
                    {
                        opened.Add(item.Name);
                    }
                }
            }

            return countedElements;
        }



        public List<ElementInfo> ParseText(string text)
        {
            var result = new List<ElementInfo>();
            if (string.IsNullOrEmpty(text)) return result;

            Regex regex = new Regex(START_ELEMENT + @"\w.+" + END_ELEMENT);
            Regex regexEnd = new Regex(COMPLEX_END_ELEMENT + @".+" + END_ELEMENT);
            Regex regexSimpleBegin = new Regex(START_ELEMENT + @".+" + SIMPLE_END_ELEMENT);

            Regex regexOpened = new Regex(START_ELEMENT + @"\w+ ");


            var starts = regex.Matches(text).OfType<Match>().Select(m => new ElementInfo { Name = GetElementName(m.Value), Index = m.Index, State = Status.Start }).ToArray();
            var ends = regexEnd.Matches(text).OfType<Match>().Select(m => new ElementInfo { Name = GetElementName(m.Value), Index = m.Index, State = Status.End }).ToArray();
            var startsSimple = regexSimpleBegin.Matches(text).OfType<Match>().Select(m => new ElementInfo { Name = GetElementName(m.Value), Index = m.Index, State = Status.SimpleClosed }).ToArray();

            var startsExcept = starts.Except(startsSimple, new ElementComparer());
        
            var openedEl = regexOpened.Matches(text).OfType<Match>().Select(m => new ElementInfo { Name = GetElementName(m.Value), Index = m.Index, State = Status.Opened }).ToArray();

            var openedExept = openedEl.Except(startsExcept, new ElementComparer());
          
            openedExept = openedExept.Except(startsSimple, new ElementComparer());
    
            result.AddRange(startsExcept);
            result.AddRange(startsSimple);
            result.AddRange(ends);
            result.AddRange(openedExept);

            result = result.OrderBy(it => it.Index).ToList();

            return result;
        }

        public List<string> GetAttributes(string text, int cursorPosition)
        {
            var openedElement = false;
            var startIndex = 0;

            for (int i = cursorPosition - 1; i >= 0; i--)
            {
                if (text[i] == END_ELEMENT[0])
                {
                    //we found ending element not opened
                    break;
                }
                if (text[i] == START_ELEMENT[0])
                {
                    startIndex = i;
                    openedElement = true;
                    break;
                }
            }

            if (!openedElement)
                return null;

            //just to set current element - TODO redo!!! bad approach
            //GetOptions();
            GetElements(text, cursorPosition);

            List<string> attributes = new List<string>();

            var endIndex = cursorPosition;
            //if we are not at very end of file
            if (text.Length > cursorPosition)
            {
                //get ending symbol or beginning of another element
                for (int i = cursorPosition + 1; i < text.Length; i++)
                {
                    if (text[i] == START_ELEMENT[0] || text[i] == END_ELEMENT[0])
                    {
                        endIndex = i;
                        break;
                    }
                }
            }

            var usedAttr = GetUsedAttributesInElement(text.Substring(startIndex, (endIndex - startIndex)));

            if (CurrentElement != null && CurrentElement.Attributes != null)
                attributes.AddRange(CurrentElement.Attributes.Select(it => it.Name).Except(usedAttr));

            return attributes;
        }

        public List<string> GetAttributeValues(string text, int cursorPosition, string attribute)
        {
            var values = new List<string>();

            GetElements(text, cursorPosition);

            var result = CurrentElement.Attributes.FirstOrDefault(it => it.Name == attribute);
            if (result != null)
                values = result.Values;

            return values;
        }

        /// <summary>
        /// List all attributes(its names) in given element node
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private List<string> GetUsedAttributesInElement(string value)
        {
            Regex reg = new Regex("(\\S+)\\s*=\\s*[\"]?((?:.(?![\"]?\\s + (?:\\S +)=|[> \"]))+.)[\"]?");
            var matches = reg.Matches(value);

            var attributes = new List<string>();
            foreach (var item in matches)
            {
                var it = item.ToString();
                attributes.Add(it.Substring(0, it.IndexOf('=')).Trim());
            }

            return attributes;
        }


        private static string GetElementName(string value)
        {

            int spacePosition = value.IndexOf(' ');

            string result = value.Remove(value.Length - 1);
            if (spacePosition > 0 && result.Length > spacePosition)
            {
                result = result.Remove(spacePosition);
            }


            if (value.StartsWith(COMPLEX_END_ELEMENT))
            {
                result = result.Remove(0, 2);
            }
            else if (value.StartsWith(START_ELEMENT))
            {
                result = result.Remove(0, 1);
            }

            return result;
        }
    }
}
